package ru.chaykin.rendering;

import static ru.chaykin.rendering.CullingDataThread.GEO_BUFF_SIZE;
import static ru.chaykin.rendering.CullingTextThread.TEX_BUFF_SIZE;

import java.nio.FloatBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import ru.chaykin.measures.IMeasure;
import ru.chaykin.measures.Mark;
import ru.chaykin.primitives.Point;
import ru.chaykin.util.Region;
import ru.chaykin.viewobject.geometry.Geometry;
import ru.chaykin.viewobject.texture.TextureFactory;
import android.opengl.GLSurfaceView;

public class OpenGLSceneRenderer implements GLSurfaceView.Renderer {

	//TODO В настройках предусмотреть включение/выключение прилипания и расстояние, с которого начинаем прилипать
	FloatBuffer mark;
	
	private static final float ADJASTING_RADIUS = 1;	//TODO Вынести в настройки радиус прилипания
	private static final float bottom = -1.0f;	//Нижняя плоскость отсечения
	private static final float top = 1.0f;		//Верхняя плоскость отсечения 
	private static final float near = 1.0f; 	//Ближняя плоскость отсечения в функции glFrustumf

	public static final int OBJECT_NOT_SELECTED = -1;
	
	private static float far = 10.0f;		//Дальняя плоскость отсечения
	
	private static int w;
	private static int h;
	
	private float dx = 0;
	private float dy = 0;
	private static float posX = 0;
	private static float posY = 0;
	private volatile static float translateX = 0;
	private volatile static float translateY = 0;
	private static float translateZ = -1.2f;
	private static GL10 gl;
	
	private IMeasure measure;

	private static float ratio;
	
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		this.gl = gl;
		gl.glLoadIdentity();
		gl.glEnable (GL10.GL_DEPTH_TEST);
		TextureFactory.genTexIds(TEX_BUFF_SIZE, gl);
	}
	
	public void onSurfaceChanged(GL10 gl, int w, int h) {
		this.w = w;
		this.h = h;
		ratio = (float) w / h;
		gl.glViewport(0, 0, w, h);
	    gl.glMatrixMode(GL10.GL_PROJECTION);
	    gl.glLoadIdentity();
	    gl.glFrustumf(-ratio, ratio, bottom, top, near, far);
	   
	    // Обнуляем буфер текстур. Если не обнулять, как-то странно все работать начинает 
	    for (int i = 0; i < TEX_BUFF_SIZE; i++) {
			synchronized(CullingTextThread.textureBuff) {
				CullingTextThread.textureBuff[i] = null;
			}
	    }
	}
	
	public static void initRenderedArea() {
		// Применяем начальное положение камеры
	    synchronized (CullingDataThread.ra) { 
	    	posX = -CullingDataThread.getRenderedArea().get_x();
	    	posY = -CullingDataThread.getRenderedArea().get_y();
	    	translateZ = -CullingDataThread.getRenderedArea().get_r();
	    	far = -translateZ + 0.01f;
	    }
	    if (gl != null) {
	    	gl.glFrustumf(-ratio, ratio, bottom, top, near, far);
	    }
	}
	
	public float getGL_X(float x) {
		return -(2 * ratio * x / w - ratio) * translateZ / near - translateX;
	}

	public float getGL_Y(float y) {
		return (2 * y / h - 1) * translateZ / near - translateY;
	}
	
	/** 
	 * Метод всегда возвращает 0, т. к. все объекты рисуются в плоскости z = 0
	 */
	public float getGL_Z(int notUsed) {
		return 0;
	}
	
	public void setPoint(float x, float y) {
		mark = Mark.getMark(adjastPoint(getGL_X(x), getGL_Y(y)), 0, -translateZ / 24.0f);
	}
	
	public void clearMark() {
		mark = null;
	}
	
	// Сглаживание - хорошая штука, но как-то сложно реализуется на Android + ресурсы телефона и без того на пределе...
	public void onDrawFrame(GL10 gl) {
		gl.glClearColor(1f, 1f, 1f, 1f);
		gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
		
		gl.glMatrixMode(GL10.GL_MODELVIEW);
		gl.glLoadIdentity();
		translateX = posX + dx;
		translateY = posY + dy;
		
		gl.glTranslatef(translateX, translateY, translateZ);
		
		gl.glDisable(GL10.GL_DEPTH_TEST);
		gl.glEnable(GL10.GL_TEXTURE_2D);
		gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
		gl.glEnable(GL10.GL_BLEND);
		
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		for (int i = 0; i < TEX_BUFF_SIZE; i++) {
			synchronized(CullingTextThread.textureBuff) {
				if (CullingTextThread.textureBuff[i] != null) 
				{
					TextureFactory.bindTextureIfNeeded(gl, i);
					CullingTextThread.textureBuff[i].draw(gl);
				}
			}
		}
		gl.glDisable(GL10.GL_BLEND);
		gl.glDisable(GL10.GL_TEXTURE_2D);
		gl.glEnable(GL10.GL_DEPTH_TEST);
		
		for (int j = 0; j < GEO_BUFF_SIZE; j++) {
			synchronized(CullingDataThread.geoBuff) {
				if (CullingDataThread.geoBuff[j] != null) {
					CullingDataThread.geoBuff[j].draw(gl);
				}
			}
		}
		if (mark != null)
			drawMark(gl);
		if (measure != null)
			measure.draw(gl);
		
	    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
	}
	
	/**
	 * Метод выравнивает точку до ближайшей видимой вершины, если она ближе радиуса "прилипания"
	 */
	private float[] adjastPoint(float x, float y) {
		float minRad = Float.MAX_VALUE;
		float dst;
		float[] adjPoint = new float[2];
		FloatBuffer buff;
		for (int j = 0; j < GEO_BUFF_SIZE; j++) {
			synchronized(CullingDataThread.geoBuff) {
				if (CullingDataThread.geoBuff[j] != null)
					buff = ((Geometry) CullingDataThread.geoBuff[j]).getBuff();
				else
					buff = null;
			}
			if (buff != null) {
				for (int i = 0; i < buff.position(); i += 3) {
					dst = Point.distance(x, y, buff.get(i), buff.get(i + 1));
					if (dst < minRad) {
						minRad = dst;
						adjPoint[0] = buff.get(i);
						adjPoint[1] = buff.get(i + 1);
					}
				}
			}
		}
		if (minRad >= ADJASTING_RADIUS) {	//Если точка дальше, чем радиус "прилипания", то не меняем координаты точки
			adjPoint[0] = x;
			adjPoint[1] = y;
		}
		return adjPoint;
	}
	
	public int getSelectedObjectId(float x, float y) {
		x = getGL_X(x);
		y = getGL_Y(y);
		float minRad = Float.MAX_VALUE;
		float minRegionRad = Float.MAX_VALUE;
		float dst;
		int id = OBJECT_NOT_SELECTED;
		int regionId = OBJECT_NOT_SELECTED;
		FloatBuffer buff;
		boolean isRegion = false;
		for (int j = 0; j < GEO_BUFF_SIZE; j++) {
			synchronized(CullingDataThread.geoBuff) {
				if (CullingDataThread.geoBuff[j] != null) {
					buff = ((Geometry) CullingDataThread.geoBuff[j]).getBuff();
					isRegion = CullingDataThread.geoBuff[j].isRegion();
					CullingDataThread.geoBuff[j].setSelected(false);
				}
				else
					buff = null;
			}
			if (buff != null) {
				if (isRegion && Region.pointInRegion(new Point(x, y), buff)) {
					for (int i = 0; i < buff.position(); i += 3) {
						dst = Point.distance(x, y, buff.get(i), buff.get(i + 1));
						if (dst < minRegionRad) {
							minRegionRad = dst;
							regionId = j;
						}
					}
				}
				
				if (!isRegion)
				for (int i = 0; i < buff.position(); i += 3) {
					dst = Point.distance(x, y, buff.get(i), buff.get(i + 1));
					if (dst < minRad) {
						minRad = dst;
						id = j;
					}
				}
			}
		}
		if (id != OBJECT_NOT_SELECTED && minRad < ADJASTING_RADIUS) {
			synchronized(CullingDataThread.geoBuff) {
				if (CullingDataThread.geoBuff[id] != null) {
					CullingDataThread.geoBuff[id].setSelected(true);
					id = CullingDataThread.geoBuff[id].getMIDIndex();
				}
			}
			return id;
		}
		if (regionId != OBJECT_NOT_SELECTED) {
			synchronized(CullingDataThread.geoBuff) {
				if (CullingDataThread.geoBuff[regionId] != null) {
					CullingDataThread.geoBuff[regionId].setSelected(true);
					regionId = CullingDataThread.geoBuff[regionId].getMIDIndex();
				}
			}
			return regionId;
		}
		return OBJECT_NOT_SELECTED;
	}
	
	public void clearSelection() {
		for (int j = 0; j < GEO_BUFF_SIZE; j++) {
			synchronized(CullingDataThread.geoBuff) {
				if (CullingDataThread.geoBuff[j] != null) 
					CullingDataThread.geoBuff[j].setSelected(false);
			}
		}
	}
	
	private final void drawMark(GL10 gl) {
		gl.glColor4f(0.0f, 0.0f, 0.0f, 0.0f);
	    gl.glLineWidth(2);
	    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mark);
		gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, 9);
		gl.glColor4f(0.0f, 0.5f, 0.0f, 0.9f);
		gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, 0, 9);
	}

	public void addPointToMeasure(float x, float y) {
		measure.add(new Point(adjastPoint(getGL_X(x), getGL_Y(y))));
	}
	
	public void setMeasure(IMeasure measure) {
		this.measure = measure;
	}
	
	public IMeasure getMeasure() {
		return measure;
	}
	
	public String getMeasuredData() {
		if (measure != null)
			return measure.getMeasuredData();
		return "";
	}
	
	public void move(float dx, float dy) {
		this.dx = -2 * ratio * dx / w  * translateZ / near;
		this.dy =  2 * dy / h  * translateZ / near;
		CullingThread.updateRenderedArea(-translateX, -translateY);
	}
	
	public void savePos(float dx, float dy) {
		posX += -2 * ratio * dx / w  * translateZ / near;
		posY +=  2 * dy / h  * translateZ / near;
		this.dx=0;
		this.dy=0;
	}

	public void zoom(float deltaZoom) {
		CullingThread.updateRenderedArea(1.5f * Point.distance(getGL_X(0), getGL_Y(0), getGL_X(w / 2f), getGL_Y(h / 2f)));
		translateZ += -2 * deltaZoom / h * translateZ / near;
		if (translateZ >= -near)
			translateZ = -near - 0.01f;
		if (translateZ <= -far)
			translateZ = -far + 0.01f;
		if (measure != null)
			measure.zoom(translateZ);
		mark =null;	//Стираем марку
	}
}
